/*
 * DIFSheet.java
 *
 * Created on January 28, 2008, 7:31 PM
 *
 * Copyright 2008 Clarke, Solomou & Associates Microsystems Ltd.
 *
 * This file is part of DIF
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.amnc.common.dif;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.ParseException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.TreeMap;

import com.amnc.common.dif.exception.DIFDataException;
import com.amnc.common.dif.exception.DIFDataPairParsingException;
import com.amnc.common.dif.exception.DIFKeywordException;
import com.amnc.common.dif.exception.DIFNumberPairInfoException;
import com.amnc.common.dif.exception.DIFRowExistsException;
import com.amnc.common.dif.exception.DIFStringFormatException;

/**
 * This class houses a spreadsheet. DIF files do not support multiple sheets
 * within a workbook, like Excel and ODF do, so this is the point of entry for
 * handling spreadsheets.
 *
 * @author Nathan Crause
 * @since 1.0
 * @version 1.1
 */
public final class DIFSheet {
    
    private TreeMap<Integer, DIFRow> rows = new TreeMap<Integer, DIFRow>();
    
    /**
     * Creates a new instance of DIFSheet
     * 
     * @since 1.0
     */
    public DIFSheet() {
    }
    
    /**
     * Creates a new instance of DIFSheet and reads the information from the
     * supplied input stream.
     * 
     * @param inStream the source from which to read the data
     * @throws java.io.IOException if an I/O error occurs while reading
     * @throws com.amnc.core.common.dif.exception.csam.dif.DIFKeywordException  if an anticipated keyword is
     * missing
     * @throws com.amnc.core.common.dif.exception.csam.dif.DIFDataPairParsingException if a data pair
     * (e.g. 0,0) cannot be properly parsed
     * @throws com.amnc.core.common.dif.exception.csam.dif.DIFNumberPairInfoException if the data in a data 
     * pair (which is expected to be 2 numbers) is not what is expected
     * @throws com.amnc.core.common.dif.exception.csam.dif.DIFStringFormatException if a string value is
     * malformed in the file
     * @throws java.text.ParseException if there is some problem parsing data,
     * such as dates
     * @see DIFReader#readSheet(com.csam.dif.DIFSheet, java.io.InputStream) 
     * @since 1.0
     */
    public DIFSheet(InputStream inStream) 
    throws IOException, DIFKeywordException, DIFDataPairParsingException, 
            DIFNumberPairInfoException, DIFStringFormatException,
            ParseException {
        this();
        
        DIFReader.readSheet(this, inStream);
    }
    
    /**
     * Returns a string representation of the entire sheet.
     * 
     * @return
     * @since 1.0
     */
    @Override public String toString() {
        // create the buffer
        StringBuffer ret = new StringBuffer(getClass().getName() + " {\n");

        // append the spreadsheet's title
        ret.append("    title=" + getTitle() + "\n");
        // iterate over the rows
        for (DIFRow row : getRows()) {
            ret.append("    " + row.getRowNumber() + " = {\n");
            // iterate over the cells
            for (DIFCell cell : row.getCells()) {
                try {
                    ret.append("        " + cell.getCellNumber() + "(" 
                            + cell.getCellType().name() + ") = "
                            + cell.getStringCellValue() + "\n");
                }
                catch (DIFDataException ex) {
                    // if an exception occurs here, we don't really want to
                    // throw out an exception, so instead, print the stacktrace
                    // to STDERR, and add the error to the output string
                    ex.printStackTrace();
                    ret.append(ex.toString());
                }
            }
            ret.append("    }\n");
        }
        ret.append("}");

        // all done, so return the results
        return ret.toString();
    }
    
    
    /**
     * Creates a new row at the specified row number.
     * 
     * @param rowNumber the numeric line number at which the row is to be
     * created
     * @throws com.amnc.core.common.dif.exception.csam.dif.DIFRowExistsException if the row number already
     * exists
     * @return the newly created row
     * @since 1.0
     */
    public DIFRow createRow(int rowNumber) throws DIFRowExistsException {
        // check if the row number already exists
        if (hasRow(rowNumber))
            throw new DIFRowExistsException(this, rowNumber);

        // create the new row
        DIFRow row = new DIFRow(this, rowNumber);

        // add it to the rows list
        rows.put(rowNumber, row);

        // return the result
        return row;
    }
    
    /**
     * Checks if the specified row number exists.
     * 
     * @param rowNumber the number to check for existance
     * @return true if the specified row number exists, false if otherwise
     * @since 1.0
     */
    public boolean hasRow(int rowNumber) {
        return rows.containsKey(rowNumber);
    }

    /**
     * Returns an immutable collection of the rows.
     *
     * @return collection containing the rows
     * @since 1.1
     */
    public Collection<DIFRow> getRows() {
        return Collections.unmodifiableCollection(rows.values());
    }
    
    /**
     * Convenience method which returns an iterator of the rows.
     * 
     * @return iterator of the rows
     * @since 1.0
     */
    public Iterator<DIFRow> rowIterator() {
        return getRows().iterator();
    }
    
    /**
     * Returns a count of the number of lines. Note that this is the actual
     * rows containing data, and does not include "empty" or non-initialized
     * rows. For example:
     * <pre>
     * DIFSheet sheet = new DIFSheet();
     * sheet.createRow(0);
     * sheet.createRow(10);
     *
     * System.out.println(sheet.getPhysicalNumberOfRows());
     * </pre>
     * will display 2. The last row's location may be "10", but there are no
     * rows initialized for rows 1 to 9.
     * 
     * @return
     * @since 1.0
     */
    public int getPhysicalNumberOfRows() {
        return rows.size();
    }
    
    /**
     * Returns the row number of the first active row in the sheet. If no rows
     * exist, this method will return -1.
     * 
     * @return first row number, or -1 if no rows in sheet
     * @since 1.0
     */
    public int getFirstRowNumber() {
        if (getPhysicalNumberOfRows() == 0)
            return -1;
        return rows.firstKey();
    }
    
    /**
     * Returns the row number of the last active row in the sheet. If no rows
     * exist, this method will return -1;
     * 
     * @return last row number, or -1 if no rows in sheet
     * @since 1.0
     */
    public int getLastRowNumber() {
        if (getPhysicalNumberOfRows() == 0)
            return -1;
        return rows.lastKey();
    }
    
    /**
     * Retrieves the row object at the specified row number in the sheet. If
     * no such row exists, <code>null</code> is returned.
     * 
     * @param rowNumber the row number to retrieve from the sheet
     * @return the row, or null if no such row
     * @since 1.0
     */
    public DIFRow getRow(int rowNumber) {
        return rows.get(rowNumber);
    }
    
    /**
     * Removes the specified row number from the sheet. If there is no such row,
     * this method does nothing.
     * 
     * @param rowNumber the row number to remove from the sheet.
     * @since 1.0
     */
    public void deleteRow(int rowNumber) {
        DIFRow row = rows.remove(rowNumber);
        if (row != null) row.cleanUp();
    }

    /**
     * Holds value of property title.
     */
    private String title;

    /**
     * Getter for property title.
     *
     * @return Value of property title.
     * @since 1.0
     */
    public String getTitle() {
        return this.title;
    }

    /**
     * Setter for property title.
     *
     * @param title New value of property title.
     * @since 1.0
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * Holds value of property version.
     */
    private int version;

    /**
     * Getter for property version.
     *
     * @return Value of property version.
     * @since 1.0
     */
    public int getVersion() {
        return this.version;
    }

    /**
     * Setter for property version.
     *
     * @param version New value of property version.
     * @since 1.0
     */
    public void setVersion(int version) {
        this.version = version;
    }

    /**
     * This method removes all the rows from the sheet.
     *
     * @since 1.0
     */
    protected void cleanUp() {
    	int nnn=getPhysicalNumberOfRows();
        while (getPhysicalNumberOfRows() > 0) {
            deleteRow(getLastRowNumber());
        }
    }
    
    /**
     * This method writes the data in this sheet out to an output stream.
     * 
     * @param outStream the target to which the sheet is to be written
     * @see DIFWriter#writeSheet(com.csam.dif.DIFSheet, java.io.OutputStream) 
     * @since 1.0
     */
    public void write(OutputStream outStream) {
        DIFWriter.writeSheet(this, outStream);
    }
    
}
